package cn.accjiyun.gomoku.view;

import cn.accjiyun.gomoku.MainApp;
import cn.accjiyun.gomoku.engine.CheckWin;
import cn.accjiyun.gomoku.engine.GomokuAI;
import cn.accjiyun.gomoku.model.ChessPoint;
import cn.accjiyun.gomoku.model.Mode;
import cn.accjiyun.gomoku.model.Role;
import cn.accjiyun.gomoku.view.resources.images.MyImageRes;
import cn.accjiyun.gomoku.view.resources.MyShadow;
import javafx.application.Platform;
import javafx.fxml.FXML;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.control.Button;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.SplitPane;
import javafx.scene.control.TextArea;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.AnchorPane;
import javafx.scene.paint.Color;
import javafx.scene.text.Font;

import java.util.Stack;

/**
 * Created by jiyun on 2016/9/7.
 */
public class BoardLayoutController {
    @FXML
    private SplitPane splitPane;
    @FXML
    private AnchorPane controlPane;
    @FXML
    private AnchorPane boardPane;
    @FXML
    private AnchorPane logPane;
    @FXML
    private TextArea textAreaLog;

    @FXML
    private Button buttonFirst;
    @FXML
    private Button buttonLast;
    @FXML
    private Button buttonNext;
    @FXML
    private Button buttonFinal;
    @FXML
    private Button buttonThinking;

    private Canvas canvas;
    private GraphicsContext gc;

    // Reference to the main application.
    private static MainApp MAINAPP;

    private ChessPoint[][] chessArray;
    private Stack<ChessPoint> chessStack;
    private Stack<ChessPoint> undoStack;
    private double canvasLength;

    private static boolean isThinking;
    private static ChessPoint bestMove;
    private static int maxDepth;
    private static int maxWidth;


    /**
     * Is called by the main application to give a reference back to itself.
     *
     * @param MAINAPP
     */
    public static void setMAINAPP(MainApp MAINAPP) {
        BoardLayoutController.MAINAPP = MAINAPP;
    }

    public static void setMaxDepth(int maxDepth) {
        BoardLayoutController.maxDepth = maxDepth;
    }

    public static void setMaxWidth(int maxWidth) {
        BoardLayoutController.maxWidth = maxWidth;
    }

    public void initBoard() {
        isThinking = false;
        maxDepth = 4;
        maxWidth = 16;
        drawBoard();
    }

    /**
     * Called to draw the chess board
     */
    public void drawBoard() {
        //fill color on the whole canvas
        int size = MAINAPP.getChessboard().getBoardSize() - 1;
        double stepLength = 40;
        double boardLength = size * stepLength;
        canvasLength = boardLength + 60 + 30;
        canvas = new Canvas(canvasLength, canvasLength);
        chessStack = MAINAPP.getChessboard().getChessStack();
        undoStack = MAINAPP.getChessboard().getUndoStack();
        chessArray = MAINAPP.getChessboard().getChessArray();
        adjustWindowSize(canvasLength);
        gc = canvas.getGraphicsContext2D();
        gc.setFont(new Font("Microsoft YaHei", 17));
        gc.setFill(Color.rgb(44, 154, 44));
        gc.fillRoundRect(0, 0, canvasLength - 30, canvasLength - 30, 0, 0);
        gc.setEffect(null);
        gc.setFill(Color.BLACK);
        for (int i = 1; i <= 9; i++) {
            gc.fillText(String.valueOf(i), canvasLength - 23, canvasLength - stepLength * i - 15, 25);
            char c = (char) ('A' + i - 1);
            gc.fillText(String.valueOf(c), stepLength * i - 15, canvasLength - 10, 30);
        }
        for (int i = 10; i <= size + 1; i++) {
            gc.fillText(String.valueOf(i), canvasLength - 28, canvasLength - stepLength * i - 15, 25);
            char c = (char) ('A' + i - 1);
            gc.fillText(String.valueOf(c), stepLength * i - 15, canvasLength - 10, 30);
        }

        gc.setLineWidth(3);
        double startPoint = 30;
        gc.strokeRoundRect(startPoint, startPoint, boardLength, boardLength, 0, 0);
        gc.setLineWidth(1);
        for (double i = startPoint + stepLength; i < startPoint + boardLength; i += stepLength) {
            gc.strokeLine(i, startPoint, i, startPoint + boardLength);
            gc.strokeLine(startPoint, i, startPoint + boardLength, i);
        }

        for (int i = 1; i <= 2; i++) {
            gc.fillOval(startPoint + (size * i / 4) * stepLength - 4,
                    startPoint + (size * i / 4) * stepLength - 4, 8, 8);
        }
        gc.fillOval(startPoint + (size - (size / 4)) * stepLength - 4,
                startPoint + (size - (size / 4)) * stepLength - 4, 8, 8);
        gc.fillOval(startPoint + (size / 4) * stepLength - 4,
                startPoint + (size - (size / 4)) * stepLength - 4, 8, 8);
        gc.fillOval(startPoint + (size - (size / 4)) * stepLength - 4,
                startPoint + (size / 4) * stepLength - 4, 8, 8);
        boardPane.getChildren().add(canvas);
    }

    /**
     * According to the chess board size to adjust window size.
     */
    private void adjustWindowSize(double canvasLength) {
        splitPane.setPrefHeight(canvasLength);
        splitPane.setPrefWidth(canvasLength + 450);
        controlPane.setPrefHeight(canvasLength);
        boardPane.setPrefHeight(canvasLength);
        boardPane.setPrefWidth(canvasLength);
        logPane.setPrefHeight(canvasLength);
    }

    /**
     * Called to draw all chess again
     */
    public void reDrawChess() {
        Platform.runLater(new Runnable() {
            @Override
            public void run() {
                drawBoard();
                synchronized (chessStack) {
                    if (!chessStack.empty()) {
                        int number = 0;
                        for (ChessPoint chessPoint : chessStack) {
                            if (chessPoint.getRole() == Role.BLACK) {
                                gc.setFill(Color.BLACK);
                                gc.setEffect(MyShadow.BLACK);
                            } else {
                                gc.setFill(Color.WHITE);
                                gc.setEffect(MyShadow.WHITE);
                            }

                            gc.fillOval(posToCoordinate(chessPoint.getX()) - 17, posToCoordinate(chessPoint.getY()) - 17, 34, 34);
                            if (MAINAPP.getRootLayoutController().isShowNum()) {
                                if (chessPoint.getRole() == Role.BLACK) gc.setFill(Color.WHITE);
                                else gc.setFill(Color.BLACK);
                                gc.setEffect(null);
                                drawChessNum(chessPoint, ++number);
                            }
                        }

                        ChessPoint chessPoint = chessStack.peek();
                        gc.setFill(Color.RED);
                        gc.setEffect(MyShadow.RED);
                        if (MAINAPP.getRootLayoutController().isShowNum()) drawChessNum(chessPoint, number);
                        else
                            gc.fillOval(posToCoordinate(chessPoint.getX()) - 4, posToCoordinate(chessPoint.getY()) - 4, 8, 8);
                        gc.setEffect(null);
                    }
                    checkWin();
                }
            }
        });
    }

    /**
     * draw the number on chess
     *
     * @param chessPoint
     * @param number     the correspondent number
     */
    public void drawChessNum(ChessPoint chessPoint, int number) {
        double coX = posToCoordinate(chessPoint.getX()), coY = posToCoordinate(chessPoint.getY());
        if (number < 10) gc.fillText(String.valueOf(number), coX - 6, coY + 7);
        else if (number < 100) gc.fillText(String.valueOf(number), coX - 10, coY + 7);
        else gc.fillText(String.valueOf(number), coX - 14, coY + 7);
    }

    /**
     *
     * @return true if the game have winner.
     */
    public boolean checkWin() {
        WinLine winLine = CheckWin.CheckWin(chessStack, MAINAPP.getChessboard().getBoardSize());
        if (winLine != null) {
            Platform.runLater(new Runnable() {
                @Override
                public void run() {
                    gc.setLineWidth(3);
                    gc.setEffect(MyShadow.RED);
                    gc.setStroke(Color.RED);
                    gc.strokeLine(posToCoordinate(winLine.beginX), posToCoordinate(winLine.beginY),
                            posToCoordinate(winLine.endX), posToCoordinate(winLine.endY));
                }
            });
            MAINAPP.getChessboard().setWin(true);
            return true;
        }
        MAINAPP.getChessboard().setWin(false);
        return false;
    }

    /**
     * Convent the coordinate to board position
     *
     * @param coordinate
     * @return
     */
    public int coToPosition(double coordinate) {
        return (int) ((coordinate - 30) / 40 + 1.5);
    }

    /**
     * Convent the board position to coordinate for draw chess
     *
     * @param pos
     * @return
     */
    public double posToCoordinate(int pos) {
        return (pos - 1) * 40L + 30L;
    }

    /**
     * Called to clear the board face
     */
    public void clearBoard() {
        gc.setFill(Color.WHITE);
        gc.fillRoundRect(0, 0, canvasLength, canvasLength, 0, 0);
    }

    /**
     * Give the button insert image
     */
    public void setButtonFace() {
        buttonFirst.setGraphic(MyImageRes.FIRST);
        buttonLast.setGraphic(MyImageRes.LAST);
        buttonNext.setGraphic(MyImageRes.NEXT);
        buttonFinal.setGraphic(MyImageRes.FINAL);
        buttonThinking.setGraphic(MyImageRes.THINKING);

        buttonFirst.setContentDisplay(ContentDisplay.TOP);
        buttonLast.setContentDisplay(ContentDisplay.TOP);
        buttonNext.setContentDisplay(ContentDisplay.TOP);
        buttonFinal.setContentDisplay(ContentDisplay.TOP);
        buttonThinking.setContentDisplay(ContentDisplay.TOP);
    }

    /**
     * It called when mouse click the board
     *
     * @param event
     */
    @FXML
    private void playChess(MouseEvent event) {
        if (isThinking) return;
        ChessPoint chessPoint = new ChessPoint();
        chessPoint.setX(coToPosition(event.getX()));
        chessPoint.setY(coToPosition(event.getY()));
        if (!isCanMove(chessPoint) || MAINAPP.getChessboard().isWin()) return;
        makeMove(chessPoint);
        shouldThink();
    }

    public void shouldThink() {
        if (isThinking || checkWin()) return;
        int mode = MAINAPP.getChessboard().getMode();
        if (mode == Mode.DOUBLE || chessStack.empty() && mode == Mode.BLACK
                || !chessStack.empty() && mode == 3 - chessStack.peek().getRole()) {
            handleThinking();
        }
    }

    @FXML
    private void handleFist() {
        undo(chessStack.size());
    }

    @FXML
    private void handleLast() {
        undo(1);
    }

    @FXML
    private void handleNext() {
        undo(-1);
    }

    @FXML
    private void handleFinal() {
        undo(-undoStack.size());
    }

    /**
     * if times is negative, undo times step,
     * if times is negative, redo -times step.
     *
     * @param times
     */
    private void undo(int times) {
        while (times > 0 && !chessStack.empty()) {
            ChessPoint chessPoint = chessStack.pop();
            undoStack.push(chessPoint);
            chessArray[chessPoint.getX()][chessPoint.getY()] = new ChessPoint();
            times--;
        }
        while (times < 0 && !undoStack.empty()) {
            ChessPoint chessPoint = undoStack.pop();
            chessStack.push(chessPoint);
            chessArray[chessPoint.getX()][chessPoint.getY()] = chessPoint;
            times++;
        }
        reDrawChess();
        shouldThink();
    }

    /**
     * called to hide or show the think log.
     */
    public void showLog(boolean isShowLog) {
        textAreaLog.setVisible(isShowLog);
    }

    /**
     * called to clear log info.
     */
    public void clearLog() {
        textAreaLog.clear();
    }

    @FXML
    private void handleThinking() {
        if (isThinking == true || MAINAPP.getChessboard().isWin()) return;
        else isThinking = true;
        new Thread() {
            @Override
            public void run() {
                GomokuAI gomokuAI = new GomokuAI(chessStack, MAINAPP.getChessboard().getBoardSize(), maxDepth, maxWidth);
                bestMove = gomokuAI.getBestMove();
                StringBuilder stringBuilder = new StringBuilder("Step:" + (chessStack.size() + 1));
                stringBuilder.append("\tTime: " + gomokuAI.getThinkTime() + "MS");
                stringBuilder.append("\t\tNode: " + gomokuAI.getTotalNode());
                showOnBoard(stringBuilder);
                makeMove(bestMove);
                isThinking = false;
                shouldThink();
            }
        }.start();
    }

    public boolean isCanMove(ChessPoint chessPoint) {
        int boradSize = MAINAPP.getChessboard().getBoardSize();
        if (chessArray[chessPoint.getX()][chessPoint.getY()].getRole() != Role.EMPTY
                || chessPoint.getX() < 1 || chessPoint.getY() < 1
                || chessPoint.getX() > boradSize || chessPoint.getY() > boradSize) {
            return false;
        }
        return true;
    }

    /**
     * Called to make chess on board.
     *
     * @param chessPoint
     */
    public void makeMove(ChessPoint chessPoint) {
        undoStack.clear();
        makeRole(chessPoint);
        chessArray[chessPoint.getX()][chessPoint.getY()] = chessPoint;
        chessStack.push(chessPoint);
        clearBoard();
        reDrawChess();
    }

    /**
     * Called to set role for ChessPoint.
     *
     * @param chessPoint
     */
    public void makeRole(ChessPoint chessPoint) {
        if (chessStack.empty() || chessStack.peek().getRole() == Role.WHITE) {
            chessPoint.setRole(Role.BLACK);
        } else {
            chessPoint.setRole(Role.WHITE);
        }
    }

    public void showOnBoard(StringBuilder stringBuilder) {
        textAreaLog.appendText("\n" + stringBuilder);
    }
}
